Session Handoff — v1 (Review, read-only)
One controller at a time. WAITING → IN_PROGRESS → RETURNED → COMPLETED. Terminal: DECLINED / CANCELLED / EXPIRED / RECALLED. Status colour law — waiting = neutral · in-progress = terracotta · returned/completed = green · recalled/expired = amber · declined/cancelled = grey · danger red is reserved for Recall & Revoke.
Frame 1 · Initiate — session action menu390×844 · dark · placement only, one new row
9:41
Refactor auth moduledefault
Panda · MacBook Pro · ~/proj/app/cc-pocket
YOU
where does the SSE stream get assembled?
CLAUDE
It's in StreamParser.feed() in the protocol module — partial frames buffer there until a blank line terminates the event.
YOU
add a unit test for the stream parser
Bashgradle :protocol:test42 passed
CLAUDE
Suite is green. The parser now emits exactly one event when a frame is split across chunks.
Message Claude…
New session
Rename session
Share transcript
Hand off to a colleagueNEW
Export as Markdown
Archive session
Anchor: ⋯ in the chat header (44×44). Popover 256px, r14, raised + hairline. The new row is the only one with a leading terracotta glyph; everything else stays neutral. Scrim starts under the header so the session you're handing off stays legible. NEW badge is temporary (2 releases).
Frame 2 · Handoff draft — full sheet390×1420 scrolling sheet · dark · exceeds the 844 viewport — scroll region in Compose hero = trust boundary card
Hand off this session
Refactor auth moduleClaude
~/proj/app/cc-pocket·feat/auth-refactor
Rolecontinue arrives later
Reviewread-only
Continuecan edit · later
Recipient
@FrankAdd a name or label…
Invite expires
1h4h24h
THEY WILL SEE
The full session transcriptevery message, tool call and diff
Code in the shared roots~/proj/app/cc-pocket ~/proj/app/cc-pocket-protocol
The full conversation history of this session will be visible — including anything you pasted into it.
Handoff briefautoEdit
Goal
Split AuthInterceptor so token refresh is testable without a live socket.
Done
Extracted TokenStore, moved refresh into RefreshCoordinator, 42 tests green.
Current state
Working tree clean on feat/auth-refactor, 6 files changed.
Ask
Review the concurrency around the refresh mutex before I open the PR.
Focus areas
RefreshCoordinator.kt
TokenStore.kt — persistence path
The boundary card is the hero — it sits above the fold-adjacent brief and uses the same two-list grammar as folder share. Mobile stacks the two columns; the honest strip spans full width on a raised background. Role is a two-up card segment, not a pill row, so the read-only sub-label always reads. Continue is present-but-disabled to set the roadmap. The @Frank remove glyph draws at 18px but takes a 44×44 hit slop.
Frame 3a · Invite ready — share sheet390×600 · dark · QR + short code
Invite ready
WAITING
H7QX-2MRD
expires in 23:59:12
Frank·Review·read-only·24h
Short code is the fallback for anyone who can't scan: 8 chars, mono, 27px, 0.1em tracking, ambiguous glyphs (0/O, 1/I) excluded from the alphabet. Countdown is caption-weight — informational, never alarming, until <5 min when it turns amber.
Frame 3b · Source chat — WAITING lock390×844 · dark · composer disabled, stream still scrollable
9:41
Refactor auth moduleWAITINGdefault
Panda · MacBook Pro · ~/proj/app/cc-pocket
YOU
add a unit test for the stream parser
Bashgradle :protocol:test42 passed
CLAUDE
Suite is green — the parser emits exactly one event when a frame straddles a chunk boundary. Working tree is clean on feat/auth-refactor.
Handoff created for Frank · Review · expires in 24h
Waiting for Frank to accept
Review · read-only · H7QX-2MRD · 23:57:04 left
Input locked during handoff
Waiting is neutral, not terracotta — nothing is running yet. The banner is a pinned card above the composer (not a toast) because the lock persists. Composer is dimmed to 45% and shows a lock glyph — it is visibly present, not removed, so the return to normal is obvious. Recall is the only danger-tinted control on the screen. Both banner buttons are 44pt tall.
Frame 4 · Recipient — accept preview390×1312 scrolling · dark · exceeds the 844 viewport — scroll region in Compose the trust screen; footer buttons pinned
9:41
Session handoffINVITE
PPanda · MacBook Pro
Refactor auth module
~/proj/app/cc-pocket·feat/auth-refactor
Code reviewroleReview
Read-onlyaccess levelno writes
Returns in 4hor when you finish
Runs on Panda's computerand Panda's model account — you're not billed
YOU WILL SEE
Panda's full session transcriptevery message, tool call and diff
Code in the shared roots~/proj/app/cc-pocket ~/proj/app/cc-pocket-protocol
Panda can recall control at any time. Your messages are recorded in their session.
Handoff brief
Goal
Split AuthInterceptor so token refresh is testable without a live socket.
Ask
Review the concurrency around the refresh mutex before the PR opens.
Focus areas
RefreshCoordinator.kt
TokenStore.kt — persistence path
Mirror of Frame 2, second person. Facts first (who / what / where), then the same boundary grammar, then the brief. The cost note is a row, not fine print — “runs on Panda's computer and model account” is the single question every recipient asks.
H7QX-2MRD · expired 12 min ago Ask Panda to send a new one.
PPanda · MacBook Pro
~/proj/app/cc-pocket·feat/auth-refactor
Code reviewrole
Read-onlyaccess level
Expired keeps the content visible at 32% so the recipient can still tell what they missed — but every action collapses to a single Close. Same shell serves DECLINED, CANCELLED and RECALLED with a swapped banner string and chip.
Panda can recall control at any time. Your messages are recorded in their session.
Handoff brief
Goal
Split AuthInterceptor so token refresh is testable without a live socket.
Ask
Review the concurrency around the refresh mutex before the PR opens.
Focus areas
RefreshCoordinator.kt
TokenStore.kt — persistence path
Light swaps tokens only — no layout change. Accent darkens to #C75A38 for AA on warm white; primary button text goes white. Success/muted marks keep their roles so the two lists stay distinguishable without relying on colour alone (glyph shape carries it).
Frame 6 · Recipient in control — IN_PROGRESS390×844 · dark · terracotta ribbon, actor attribution
9:41
Refactor auth moduleread-only
PFContinuing Panda's session · Review3:41:22
Panda · MacBook Pro · ~/proj/app/cc-pocketread-only tools
You joined this session for code review — see the Handoff brief
PANDA
add a unit test for the stream parser
Bashgradle :protocol:test42 passed
FRANK (YOU)
show me every call site that touches the refresh mutex
GreprefreshMutex --glob *.kt7 hits
CLAUDE
Seven call sites. Six take the mutex before reading TokenStore; RefreshCoordinator.kt:88 reads it outside the lock
where else is the store read?
Terracotta means in progress — the ribbon is the only tinted surface. Attribution appears only once a session has had more than one actor; solo sessions keep the plain YOU / CLAUDE labels. The recipient's own label carries a 5px accent tick. Tool rows are untouched — the actor of a tool call is always the agent.
show me every call site that touches the refresh mutex
GreprefreshMutex --glob *.kt7 hits
CLAUDE
Seven call sites. Six take the mutex before reading TokenStore; RefreshCoordinator.kt:88 reads it outside the lock
Watching — input locked
Same stream, different chrome. The initiator's ribbon is neutral — they are not the actor, so no terracotta. The composer is replaced here (not dimmed): during IN_PROGRESS there is no draft to preserve, and the bar carries the one escape hatch. Streaming output keeps rendering live.
Frame 8 · Return flow — result draft390×900 · dark · structured, editable
Return session to Panda
Panda gets control back and this result is added to the session.
Return resultdraft · autoEdit
Verdict
Approve with fixesApproveRequest changes
Findings · 3
Token store read outside the refresh mutexRefreshCoordinator.kt:88
Retry backoff resets on every 401, so it never growsRefreshCoordinator.kt:142
Test covers the split-chunk case but not an empty frameStreamParserTest.kt:61
Verified
gradle :protocol:test42 passed
gradle :app:lintclean
gradle :app:test1 failed
Suggested next steps
Move the TokenStore read inside withLock.
Make backoff state survive a 401 burst.
Add an empty-frame case to the parser test.
The result is structured, not prose — verdict + findings + verification + next steps — so it can be rendered as a card, injected into the session as context, and later diffed. Severity is a 7px dot: red / amber / secondary. Every finding carries a mono file:line. Verdict chips use the status palette (amber = attention, not failure).
Frame 9 · Initiator — RETURNED, result in stream390×844 · dark · composer unlocked
9:41
Refactor auth moduleRETURNEDdefault
Panda · MacBook Pro · ~/proj/app/cc-pocket
FRANK
show me every call site that touches the refresh mutex
FFrank returned this sessionApprove with fixes
Token store read outside the refresh mutexRefreshCoordinator.kt:88
Retry backoff resets on every 401RefreshCoordinator.kt:142
Result injected into the session for Claude to continue.
Message Claude…
RETURNED is green — the loop closed successfully; the verdict chip carries its own (amber) severity independently. The card is an in-stream artefact, not a modal, so it stays scrollable in history. Mark reviewed is the only transition to COMPLETED. The system row is deliberately plain: the user should know the result became context.
Frame 10 · Session info — Handoffs390×620 · dark · status chip legend in situ
9:41
Session info
Handoffs6
FFrankReview18 min · 5 findingsIn progress
MMeiReviewyesterday · approvedCompleted
FFrankReview2 days ago · pulled back after 4 minRecalled
AAikoReview3 days agoDeclined
MMeiReviewlast week · never openedExpired
?Unsent inviteReviewlast weekCancelled
Frame 10b · Handoffs — empty390×300 · dark
Handoffs
Hand off this session to get a colleague's eyes on it.
Rows are 60px, whole row tappable, chevron trailing. The status chip is the row's only colour. In progress pulses (1.4s, opacity 1→.45) — it is the one live state. Expired and Recalled share amber because both mean “ended without a result”; Declined / Cancelled are grey — a non-event.
Desktop — 1200pt two-paneLeft session sidebar 288px · right chat pane. Same tokens, same status law, denser rhythm.
The full conversation history of this session will be visible — including anything you pasted into it.
Handoff briefautoEdit
Goal
Split AuthInterceptor so token refresh is testable without a live socket.
Ask
Review the concurrency around the refresh mutex before I open the PR.
Your input locks until Frank returns
Desktop keeps the mobile information order but splits it across two columns: decisions left, consequences right. The boundary card goes back to its native two-up form. The dialog footer restates the cost of the action next to the primary button — desktop users click faster. Sidebar: the active session row gains a status dot (neutral = waiting) and a lock glyph; other rows are unaffected, so a handoff never disturbs the rest of the fleet.
Frame 12 · Desktop — recipient in control + result1200×800 · dark · terracotta ribbon, two-column findings
Panda · MacBook Pro guest
Refactor auth module~/proj/app/cc-pocket · you have control
Not shared with youPanda's other sessions stay hidden
PFContinuing Panda's session · Reviewreturns in 3:41:22
PANDA
add a unit test for the stream parser
FRANK (YOU)
show me every call site that touches the refresh mutex
GreprefreshMutex --glob *.kt7 hits
FFrank returned this sessionApprove with fixes18 min
Findings · 5
Token store read outside the refresh mutexRefreshCoordinator.kt:88
Retry backoff resets on every 401RefreshCoordinator.kt:142
Refresh failure swallows the causeAuthInterceptor.kt:57
Empty-frame case missing from the parser testStreamParserTest.kt:61
Persistence path is hard-codedTokenStore.kt:24
Two unused imports after the splitAuthInterceptor.kt:1
6 files reviewed · 0 changed
protocol:test 42 ✓
app:lint clean
app:test 1 ✗
where else is the store read?
The guest's sidebar shows exactly one session plus an explicit “not shared with you” row — absence is stated, never implied. Finish & return is a pane-header button on desktop (persistent, top-right) instead of the mobile pill above the composer. The result card grows to 820px and findings go two-up; the card is capped so long results scroll inside the stream rather than stretching the pane.